{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "cPKvKMkAkRMn"
      },
      "source": [
        "##### Copyright 2019 The TensorFlow Authors."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "cellView": "form",
        "id": "TWofNaR-kS1s"
      },
      "outputs": [],
      "source": [
        "#@title Licensed under the Apache License, Version 2.0 (the \"License\");\n",
        "# you may not use this file except in compliance with the License.\n",
        "# You may obtain a copy of the License at\n",
        "#\n",
        "# https://www.apache.org/licenses/LICENSE-2.0\n",
        "#\n",
        "# Unless required by applicable law or agreed to in writing, software\n",
        "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
        "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
        "# See the License for the specific language governing permissions and\n",
        "# limitations under the License."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "xWVObL142EBs"
      },
      "source": [
        "# Написание пользовательских слоев и моделей с Keras"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "p7VqaVrAvw9j"
      },
      "source": [
        "<table class=\"tfo-notebook-buttons\" align=\"left\">\n",
        "  <td>\n",
        "    <a target=\"_blank\" href=\"https://www.tensorflow.org/guide/keras/custom_layers_and_models\"><img src=\"https://www.tensorflow.org/images/tf_logo_32px.png\" />Смотрите на TensorFlow.org</a>\n",
        "  </td>\n",
        "  <td>\n",
        "    <a target=\"_blank\" href=\"https://colab.research.google.com/github/tensorflow/docs-l10n/blob/master/site/ru/guide/keras/custom_layers_and_models.ipynb\"><img src=\"https://www.tensorflow.org/images/colab_logo_32px.png\" />Запустите в Google Colab</a>\n",
        "  </td>\n",
        "  <td>\n",
        "    <a target=\"_blank\" href=\"https://github.com/tensorflow/docs-l10n/blob/master/site/ru/guide/keras/custom_layers_and_models.ipynb\"><img src=\"https://www.tensorflow.org/images/GitHub-Mark-32px.png\" />Изучайте код на GitHub</a>\n",
        "  </td>\n",
        "  <td>\n",
        "    <a href=\"https://storage.googleapis.com/tensorflow_docs/docs-l10n/site/ru/guide/keras/custom_layers_and_models.ipynb\"><img src=\"https://www.tensorflow.org/images/download_logo_32px.png\" />Скачайте ноутбук</a>\n",
        "  </td>\n",
        "</table>"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "fj66ZXAzrJC2"
      },
      "source": [
        "Note: Вся информация в этом разделе переведена с помощью русскоговорящего Tensorflow сообщества на общественных началах. Поскольку этот перевод не является официальным, мы не гарантируем что он на 100% аккуратен и соответствует [официальной документации на английском языке](https://www.tensorflow.org/?hl=en). Если у вас есть предложение как исправить этот перевод, мы будем очень рады увидеть pull request в [tensorflow/docs](https://github.com/tensorflow/docs) репозиторий GitHub. Если вы хотите помочь сделать документацию по Tensorflow лучше (сделать сам перевод или проверить перевод подготовленный кем-то другим), напишите нам на [docs-ru@tensorflow.org list](https://groups.google.com/a/tensorflow.org/forum/#!forum/docs-ru)."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "NrUIvL8oxlhj"
      },
      "source": [
        "### Установка"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Szd0mNROxqJ7"
      },
      "outputs": [],
      "source": [
        "from __future__ import absolute_import, division, print_function, unicode_literals\n",
        "\n",
        "try:\n",
        "  # %tensorflow_version only exists in Colab.\n",
        "  %tensorflow_version 2.x\n",
        "except Exception:\n",
        "  pass\n",
        "import tensorflow as tf\n",
        "\n",
        "tf.keras.backend.clear_session()  # Для легкого сброса состояния ноутбука."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "D0KRUQWG2k4v"
      },
      "source": [
        "### Класс Layer это комбинация состояние (веса) и некоторых вычислений\n",
        "\n",
        "Основной абстракцией Keras является класс `Layer`. `Layer` инакпсулирует в себе две вещи - состояние (\"веса\") слоя\n",
        "и преобразование входящих данных в исходящие.\n",
        "Ниже представлен пример полносвязного слоя. У него есть состояние: переменные `w` и `b`.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "LisHKABR2-Nj"
      },
      "outputs": [],
      "source": [
        "from tensorflow.keras import layers\n",
        "\n",
        "\n",
        "class Linear(layers.Layer):\n",
        "\n",
        "  def __init__(self, units=32, input_dim=32):\n",
        "    super(Linear, self).__init__()\n",
        "    w_init = tf.random_normal_initializer()\n",
        "    self.w = tf.Variable(initial_value=w_init(shape=(input_dim, units),\n",
        "                                              dtype='float32'),\n",
        "                         trainable=True)\n",
        "    b_init = tf.zeros_initializer()\n",
        "    self.b = tf.Variable(initial_value=b_init(shape=(units,),\n",
        "                                              dtype='float32'),\n",
        "                         trainable=True)\n",
        "\n",
        "  def call(self, inputs):\n",
        "    return tf.matmul(inputs, self.w) + self.b\n",
        "\n",
        "x = tf.ones((2, 2))\n",
        "linear_layer = Linear(4, 2)\n",
        "y = linear_layer(x)\n",
        "print(y)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Y8RsI6Hr2OOd"
      },
      "source": [
        "Обратите внимание, что веса` w` и `b` автоматически отслеживаются слоем\n",
        "после их установки в качестве атрибутов слоя:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "D7x3Hl8m2XEJ"
      },
      "outputs": [],
      "source": [
        "assert linear_layer.weights == [linear_layer.w, linear_layer.b]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "IXQPwqEs2gCH"
      },
      "source": [
        "Также у вас есть более быстрый способ добавления веса к слою: метод` add_weight`:\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "8LSx6HDg2iPz"
      },
      "outputs": [],
      "source": [
        "class Linear(layers.Layer):\n",
        "\n",
        "  def __init__(self, units=32, input_dim=32):\n",
        "    super(Linear, self).__init__()\n",
        "    self.w = self.add_weight(shape=(input_dim, units),\n",
        "                             initializer='random_normal',\n",
        "                             trainable=True)\n",
        "    self.b = self.add_weight(shape=(units,),\n",
        "                             initializer='zeros',\n",
        "                             trainable=True)\n",
        "\n",
        "  def call(self, inputs):\n",
        "    return tf.matmul(inputs, self.w) + self.b\n",
        "\n",
        "x = tf.ones((2, 2))\n",
        "linear_layer = Linear(4, 2)\n",
        "y = linear_layer(x)\n",
        "print(y)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "vXjjEthGgr4y"
      },
      "source": [
        "#### У слоев могут быть необучаемые веса\n",
        "\n",
        "Кроме обучаемых весов, вы также можете добавить к слою не обучаемые веса.\n",
        "Такие веса не должны учитываться при обратном распространении ошибки,\n",
        ", когда вы тренируете слой.\n",
        "\n",
        "Вот так можно добавить и использовать не обучаемый вес:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "OIIfmpDIgyUy"
      },
      "outputs": [],
      "source": [
        "class ComputeSum(layers.Layer):\n",
        "\n",
        "  def __init__(self, input_dim):\n",
        "    super(ComputeSum, self).__init__()\n",
        "    self.total = tf.Variable(initial_value=tf.zeros((input_dim,)),\n",
        "                             trainable=False)\n",
        "\n",
        "  def call(self, inputs):\n",
        "    self.total.assign_add(tf.reduce_sum(inputs, axis=0))\n",
        "    return self.total\n",
        "\n",
        "x = tf.ones((2, 2))\n",
        "my_sum = ComputeSum(2)\n",
        "y = my_sum(x)\n",
        "print(y.numpy())\n",
        "y = my_sum(x)\n",
        "print(y.numpy())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "qLiWVq-3g0c0"
      },
      "source": [
        "Это часть `layer.weights`, но он классифицируется как необучаемый вес:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "X7RhEZNvg2dE"
      },
      "outputs": [],
      "source": [
        "print('weights:', len(my_sum.weights))\n",
        "print('non-trainable weights:', len(my_sum.non_trainable_weights))\n",
        "\n",
        "# Это не включено в обучаемы веса:\n",
        "print('trainable_weights:', my_sum.trainable_weights)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "DOwYZ-Ew329E"
      },
      "source": [
        "### Хорошая практика: откладывать создание весов до тех пор, пока не станет известна форма входных данных\n",
        "\n",
        "В приведенном выше примере с логистической регрессией, наш слой `Linear` layer принимает аргумент `input_dim`\n",
        "который был использован чтобы посчитать размерности весов `w` и `b` в `__init__`:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "tzxUxoPc3Esh"
      },
      "outputs": [],
      "source": [
        "class Linear(layers.Layer):\n",
        "\n",
        "  def __init__(self, units=32, input_dim=32):\n",
        "      super(Linear, self).__init__()\n",
        "      self.w = self.add_weight(shape=(input_dim, units),\n",
        "                               initializer='random_normal',\n",
        "                               trainable=True)\n",
        "      self.b = self.add_weight(shape=(units,),\n",
        "                               initializer='zeros',\n",
        "                               trainable=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ejSYZGaP4CD6"
      },
      "source": [
        "Часто вы можете не знать заранее размер ваших входных данных, и вы хотели\n",
        "бы лениво создавать веса, когда это значение станет известным,\n",
        "через какое-то время после создания экземпляра слоя.\n",
        "\n",
        "В Keras API мы рекомендуем создавать веса в методе `build(input_shape)` вашего слоя.\n",
        "Как в этом примере:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "AGhRg7Nt4EB8"
      },
      "outputs": [],
      "source": [
        "class Linear(layers.Layer):\n",
        "\n",
        "  def __init__(self, units=32):\n",
        "    super(Linear, self).__init__()\n",
        "    self.units = units\n",
        "\n",
        "  def build(self, input_shape):\n",
        "    self.w = self.add_weight(shape=(input_shape[-1], self.units),\n",
        "                             initializer='random_normal',\n",
        "                             trainable=True)\n",
        "    self.b = self.add_weight(shape=(self.units,),\n",
        "                             initializer='random_normal',\n",
        "                             trainable=True)\n",
        "\n",
        "  def call(self, inputs):\n",
        "    return tf.matmul(inputs, self.w) + self.b"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "0SpZzAag4Mk_"
      },
      "source": [
        "Метод `__call__` вашего слоя автоматически запускает `build` при первом вызове.\n",
        "Теперь у вас есть слой, который ленив и прост в использовании:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "_cdMFCUp4KSQ"
      },
      "outputs": [],
      "source": [
        "linear_layer = Linear(32)  # При создании экземпляра мы еще не знаем размерность входа, на которой он будет вызываться\n",
        "y = linear_layer(x)  # Веса слоя создается динамически при первом вызове"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "-kaDooBSC_Oc"
      },
      "source": [
        "### Слои рекурсивно компонуемы\n",
        "\n",
        "Если вы присвоите экземпляр слоя как атрибут другого слоя,\n",
        "внешний слой начнет отслеживать веса внутреннего.\n",
        "\n",
        "Мы рекомендуем создавать такие подслои в методе `__init__` (поскольку подслои обычно имеют метод `build`, они будут собраны, когда будет собран внешний слой)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "-YPI4vwN4Ozo"
      },
      "outputs": [],
      "source": [
        "# Предположим что мы переиспользуем класс Linear\n",
        "# с методом `build` который мы определили выше.\n",
        "\n",
        "class MLPBlock(layers.Layer):\n",
        "\n",
        "  def __init__(self):\n",
        "    super(MLPBlock, self).__init__()\n",
        "    self.linear_1 = Linear(32)\n",
        "    self.linear_2 = Linear(32)\n",
        "    self.linear_3 = Linear(1)\n",
        "\n",
        "  def call(self, inputs):\n",
        "    x = self.linear_1(inputs)\n",
        "    x = tf.nn.relu(x)\n",
        "    x = self.linear_2(x)\n",
        "    x = tf.nn.relu(x)\n",
        "    return self.linear_3(x)\n",
        "\n",
        "\n",
        "mlp = MLPBlock()\n",
        "y = mlp(tf.ones(shape=(3, 64)))  # Первый вызов `mlp` создаст веса\n",
        "print('weights:', len(mlp.weights))\n",
        "print('trainable weights:', len(mlp.trainable_weights))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "fq5_AsbEh-BQ"
      },
      "source": [
        "### Слои рекурсивно собирают потери, созданные во время прямого прохода\n",
        "\n",
        "При написании метода слоя `call` вы можете создать тензоры потерь, которые вы можете использовать позже, при написании цикла обучения. Это можно сделать путем вызова `self.add_loss (value)`:\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "W66HsCzajERu"
      },
      "outputs": [],
      "source": [
        "# Слой, создающий activity regularization loss\n",
        "class ActivityRegularizationLayer(layers.Layer):\n",
        "\n",
        "  def __init__(self, rate=1e-2):\n",
        "    super(ActivityRegularizationLayer, self).__init__()\n",
        "    self.rate = rate\n",
        "\n",
        "  def call(self, inputs):\n",
        "    self.add_loss(self.rate * tf.reduce_sum(inputs))\n",
        "    return inputs"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "p_dMrJ-QjcZH"
      },
      "source": [
        "Значения потерь (в том числе созданные любым внутренним слоем) могут быть извлечены с помощью `layer.losses`.\n",
        "Это свойство сбрасывается в начале каждого` __call__` на слой верхнего уровня, так что `layer.losses` всегда содержит значения потерь, созданные во время последнего прохода."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "C1vYiSnVjdCc"
      },
      "outputs": [],
      "source": [
        "class OuterLayer(layers.Layer):\n",
        "\n",
        "  def __init__(self):\n",
        "    super(OuterLayer, self).__init__()\n",
        "    self.activity_reg = ActivityRegularizationLayer(1e-2)\n",
        "\n",
        "  def call(self, inputs):\n",
        "    return self.activity_reg(inputs)\n",
        "\n",
        "\n",
        "layer = OuterLayer()\n",
        "assert len(layer.losses) == 0  # Потерь пока нет, так как слой не вызываался\n",
        "_ = layer(tf.zeros(1, 1))\n",
        "assert len(layer.losses) == 1  # Мы создали одно значение потерь\n",
        "\n",
        "# `layer.losses` сбрасывается в начале каждого __call__\n",
        "_ = layer(tf.zeros(1, 1))\n",
        "assert len(layer.losses) == 1  # Это потери созданные во время вызова выше"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "9Jv3LKNfk_LL"
      },
      "source": [
        "Кроме того, свойство` loss` также содержит потери регуляризации, созданные для весов любого внутреннего слоя:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "iokhhZfUlJUU"
      },
      "outputs": [],
      "source": [
        "class OuterLayer(layers.Layer):\n",
        "\n",
        "  def __init__(self):\n",
        "    super(OuterLayer, self).__init__()\n",
        "    self.dense = layers.Dense(32, kernel_regularizer=tf.keras.regularizers.l2(1e-3))\n",
        "\n",
        "  def call(self, inputs):\n",
        "    return self.dense(inputs)\n",
        "\n",
        "\n",
        "layer = OuterLayer()\n",
        "_ = layer(tf.zeros((1, 1)))\n",
        "\n",
        "# Это `1e-3 * sum(layer.dense.kernel ** 2)`,\n",
        "# созданное `kernel_regularizer` выше.\n",
        "print(layer.losses)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "P2Xdp_dvlGLG"
      },
      "source": [
        "Предполагается, что эти потери будут учитываться при написании циклов обучения, например:\n",
        "\n",
        "\n",
        "```python\n",
        "# Создаем экземпляр оптимизатора.\n",
        "optimizer = tf.keras.optimizers.SGD(learning_rate=1e-3)\n",
        "loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)\n",
        "\n",
        "# Итерация по батчам набора данных.\n",
        "for x_batch_train, y_batch_train in train_dataset:\n",
        "  with tf.GradientTape() as tape:\n",
        "    logits = layer(x_batch_train)  # Логиты для этого минибатча\n",
        "    # Значения потерь для этого минибатча\n",
        "    loss_value = loss_fn(y_batch_train, logits)\n",
        "    # Добавим дополнительные потери созданные во время этого прохода:\n",
        "    loss_value += sum(model.losses)\n",
        "\n",
        "  grads = tape.gradient(loss_value, model.trainable_weights)\n",
        "  optimizer.apply_gradients(zip(grads, model.trainable_weights))\n",
        "```\n",
        "\n",
        "Подробное руководство по написанию циклов обучения см. во втором разделе [руководства по обучению и оценке](./train_and_evaluate.ipynb)."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ozo04iqHohNg"
      },
      "source": [
        "### Вы можете опционально включить сериализацию на своих слоях\n",
        "\n",
        "Если вам нужно, чтобы ваш кастомный слой был сериализуем как часть [Functional model](./functional.ipynb), вы можете опционально реализовать метод `get_config`:\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ckT5Zbo0oxrz"
      },
      "outputs": [],
      "source": [
        "class Linear(layers.Layer):\n",
        "\n",
        "  def __init__(self, units=32):\n",
        "    super(Linear, self).__init__()\n",
        "    self.units = units\n",
        "\n",
        "  def build(self, input_shape):\n",
        "    self.w = self.add_weight(shape=(input_shape[-1], self.units),\n",
        "                             initializer='random_normal',\n",
        "                             trainable=True)\n",
        "    self.b = self.add_weight(shape=(self.units,),\n",
        "                             initializer='random_normal',\n",
        "                             trainable=True)\n",
        "\n",
        "  def call(self, inputs):\n",
        "    return tf.matmul(inputs, self.w) + self.b\n",
        "\n",
        "  def get_config(self):\n",
        "    return {'units': self.units}\n",
        "\n",
        "\n",
        "# Сейчас вы можете пересоздать слой из его конфигурации:\n",
        "layer = Linear(64)\n",
        "config = layer.get_config()\n",
        "print(config)\n",
        "new_layer = Linear.from_config(config)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "9fKngh4UozyM"
      },
      "source": [
        "Обратите внимание, что метод` __init__` базового класса `Layer` принимает некоторые аргументы, в частности` name` и `dtype`. Рекомендуется передать эти аргументы родительскому классу в` __init__` и добавить их в конфиг слоя:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "UCMoN42no0D5"
      },
      "outputs": [],
      "source": [
        "class Linear(layers.Layer):\n",
        "\n",
        "  def __init__(self, units=32, **kwargs):\n",
        "    super(Linear, self).__init__(**kwargs)\n",
        "    self.units = units\n",
        "\n",
        "  def build(self, input_shape):\n",
        "    self.w = self.add_weight(shape=(input_shape[-1], self.units),\n",
        "                             initializer='random_normal',\n",
        "                             trainable=True)\n",
        "    self.b = self.add_weight(shape=(self.units,),\n",
        "                             initializer='random_normal',\n",
        "                             trainable=True)\n",
        "\n",
        "  def call(self, inputs):\n",
        "    return tf.matmul(inputs, self.w) + self.b\n",
        "\n",
        "  def get_config(self):\n",
        "    config = super(Linear, self).get_config()\n",
        "    config.update({'units': self.units})\n",
        "    return config\n",
        "\n",
        "\n",
        "layer = Linear(64)\n",
        "config = layer.get_config()\n",
        "print(config)\n",
        "new_layer = Linear.from_config(config)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "sNrHV0zAo0Tc"
      },
      "source": [
        "Если вам требуется больше гибкости при десериализации слоя из его конфига, вы также можете переопределить метод класса` from_config`. Это базовая реализация `from_config`:\n",
        "\n",
        "```python\n",
        "def from_config(cls, config):\n",
        "  return cls(**config)\n",
        "```\n",
        "\n",
        "Чтобы узнать больше о сериализации и сохранении см. полное [Руководство по сохранению и сериализации моделей](./save_and_serialize.ipynb)."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "-TB8iViSo4p9"
      },
      "source": [
        "### Привилегированный аргумент `training` в методе `call`\n",
        "\n",
        "\n",
        "Некоторые уровни, в частности слои `BatchNormalization` и `Dropout`, имеют различное поведение во время обучения и вывода. Для таких слоев стандартной практикой является представление (булева) аргумента `training` в методе `call`.\n",
        "\n",
        "Представляя этот аргумент в `call`, вы позволяете встроенным циклам обучения и оценки (например, `fit`) правильно использовать слой в обучении и выводе.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "QyI_b4Rgo-EE"
      },
      "outputs": [],
      "source": [
        "class CustomDropout(layers.Layer):\n",
        "\n",
        "  def __init__(self, rate, **kwargs):\n",
        "    super(CustomDropout, self).__init__(**kwargs)\n",
        "    self.rate = rate\n",
        "\n",
        "  def call(self, inputs, training=None):\n",
        "    if training:\n",
        "        return tf.nn.dropout(inputs, rate=self.rate)\n",
        "    return inputs"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "3X6eQH_K2wf1"
      },
      "source": [
        "## Построение моделей"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "XZen-bAOE9I5"
      },
      "source": [
        "### Класс Model\n",
        "\n",
        "В основном вы будете использовать класс` Layer` для определения внутренних вычислительных блоков,\n",
        "а класс `Model` для определения внешней модели - объекта, который вы будете обучать.\n",
        "\n",
        "Например, в модели ResNet50, у вас будет несколько блоков ResNet наследующихся от `Layer`,\n",
        "и единственный класс `Model` охватывающий всю сеть ResNet50.\n",
        "\n",
        "У класса `Model` тот же API что и у `Layer`, со следующими раазличиями:\n",
        "\n",
        "- `Model` предоставляет встроенные циклы обучения, оценки и прогнозирования (`model.fit()`, `model.evaluate()`, `model.predict()`).\n",
        "- `Model` предоставляет список своих внутренних слоев с помощью свойства `model.layers`.\n",
        "- `Model` предоставляет API сохранения и сериализации.\n",
        "\n",
        "По сути, класс \"Layer\" соответствует тому, что мы называем в литературе\n",
        "\"слой\" (как в \"сверточный слой\" или \"реккурентный слой\") или \"блок\" (как в \"блок ResNet\" или \"Inception block\").\n",
        "\n",
        "Между тем, класс \"Model\" соответствует тому, что мы называем в литературе\n",
        "\"модель\" (как в \"модель глубокого обучения\") или \"сеть\" (как в \"нейронная сеть\").\n",
        "\n",
        "Например, мы можем взять наш вышеприведенный мини-resnet пример, и использовать его для построения `Model` которую мы можем обучить с помощью `fit()` и сохранить, используя `save_weights`:\n",
        "\n",
        "```python\n",
        "class ResNet(tf.keras.Model):\n",
        "\n",
        "    def __init__(self):\n",
        "        super(ResNet, self).__init__()\n",
        "        self.block_1 = ResNetBlock()\n",
        "        self.block_2 = ResNetBlock()\n",
        "        self.global_pool = layers.GlobalAveragePooling2D()\n",
        "        self.classifier = Dense(num_classes)\n",
        "\n",
        "    def call(self, inputs):\n",
        "        x = self.block_1(inputs)\n",
        "        x = self.block_2(x)\n",
        "        x = self.global_pool(x)\n",
        "        return self.classifier(x)\n",
        "\n",
        "\n",
        "resnet = ResNet()\n",
        "dataset = ...\n",
        "resnet.fit(dataset, epochs=10)\n",
        "resnet.save_weights(filepath)\n",
        "```\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "roVCX-TJqYzx"
      },
      "source": [
        "### Собираем все вместе: полный пример\n",
        "\n",
        "Вот то что мы выучили к этому моменту:\n",
        "\n",
        "- `Layer` инкапсулирует состояние (созданное в `__init__` или `build`) и некоторые вычисления (в `call`).\n",
        "- Слои могут быть рекурсивно вложены чтобы создать новый, больший вычислительный блок.\n",
        "- Слои могут создавать и отслеживать потери (обычно потери регуляризации).\n",
        "- Внешний контейнер, объект который вы будете обучать это `Model`. `Model` похож на `Layer`, с добавлением утилит обучения и сериализации.\n",
        "\n",
        "Давайте соединим все эти вещи в одном сквозном примере: мы собираемся реализовать вариационный автоэкодер (VAE). Мы будем обучать его на цифрах из MNIST.\n",
        "\n",
        "Наш VAE будет подклассом `Model`, построенным как вложенная композиция подклассов `Layer`. У него будут потери регуляризации (KL-дивергенция)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "1QxkfjtzE4X2"
      },
      "outputs": [],
      "source": [
        "class Sampling(layers.Layer):\n",
        "  \"\"\"Использует (z_mean, z_log_var) для выборки z, вектора кодирующего цифру.\"\"\"\n",
        "\n",
        "  def call(self, inputs):\n",
        "    z_mean, z_log_var = inputs\n",
        "    batch = tf.shape(z_mean)[0]\n",
        "    dim = tf.shape(z_mean)[1]\n",
        "    epsilon = tf.keras.backend.random_normal(shape=(batch, dim))\n",
        "    return z_mean + tf.exp(0.5 * z_log_var) * epsilon\n",
        "\n",
        "\n",
        "class Encoder(layers.Layer):\n",
        "  \"\"\"Отображает цифры MNIST в тройки (z_mean, z_log_var, z).\"\"\"\n",
        "\n",
        "  def __init__(self,\n",
        "               latent_dim=32,\n",
        "               intermediate_dim=64,\n",
        "               name='encoder',\n",
        "               **kwargs):\n",
        "    super(Encoder, self).__init__(name=name, **kwargs)\n",
        "    self.dense_proj = layers.Dense(intermediate_dim, activation='relu')\n",
        "    self.dense_mean = layers.Dense(latent_dim)\n",
        "    self.dense_log_var = layers.Dense(latent_dim)\n",
        "    self.sampling = Sampling()\n",
        "\n",
        "  def call(self, inputs):\n",
        "    x = self.dense_proj(inputs)\n",
        "    z_mean = self.dense_mean(x)\n",
        "    z_log_var = self.dense_log_var(x)\n",
        "    z = self.sampling((z_mean, z_log_var))\n",
        "    return z_mean, z_log_var, z\n",
        "\n",
        "\n",
        "class Decoder(layers.Layer):\n",
        "  \"\"\"Конвертирует z, закодированный вектор цифры обратно в читаемую цифру.\"\"\"\n",
        "\n",
        "  def __init__(self,\n",
        "               original_dim,\n",
        "               intermediate_dim=64,\n",
        "               name='decoder',\n",
        "               **kwargs):\n",
        "    super(Decoder, self).__init__(name=name, **kwargs)\n",
        "    self.dense_proj = layers.Dense(intermediate_dim, activation='relu')\n",
        "    self.dense_output = layers.Dense(original_dim, activation='sigmoid')\n",
        "\n",
        "  def call(self, inputs):\n",
        "    x = self.dense_proj(inputs)\n",
        "    return self.dense_output(x)\n",
        "\n",
        "\n",
        "class VariationalAutoEncoder(tf.keras.Model):\n",
        "  \"\"\"Соединяет энкодер и декодер в сквозную модель для обучения.\"\"\"\n",
        "\n",
        "  def __init__(self,\n",
        "               original_dim,\n",
        "               intermediate_dim=64,\n",
        "               latent_dim=32,\n",
        "               name='autoencoder',\n",
        "               **kwargs):\n",
        "    super(VariationalAutoEncoder, self).__init__(name=name, **kwargs)\n",
        "    self.original_dim = original_dim\n",
        "    self.encoder = Encoder(latent_dim=latent_dim,\n",
        "                           intermediate_dim=intermediate_dim)\n",
        "    self.decoder = Decoder(original_dim, intermediate_dim=intermediate_dim)\n",
        "\n",
        "  def call(self, inputs):\n",
        "    z_mean, z_log_var, z = self.encoder(inputs)\n",
        "    reconstructed = self.decoder(z)\n",
        "    # Добавляет потери регуляризации - KL-дивергенцию.\n",
        "    kl_loss = - 0.5 * tf.reduce_mean(\n",
        "        z_log_var - tf.square(z_mean) - tf.exp(z_log_var) + 1)\n",
        "    self.add_loss(kl_loss)\n",
        "    return reconstructed"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "oDVSVl4Iu8kC"
      },
      "outputs": [],
      "source": [
        "original_dim = 784\n",
        "vae = VariationalAutoEncoder(original_dim, 64, 32)\n",
        "\n",
        "optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)\n",
        "mse_loss_fn = tf.keras.losses.MeanSquaredError()\n",
        "\n",
        "loss_metric = tf.keras.metrics.Mean()\n",
        "\n",
        "(x_train, _), _ = tf.keras.datasets.mnist.load_data()\n",
        "x_train = x_train.reshape(60000, 784).astype('float32') / 255\n",
        "\n",
        "train_dataset = tf.data.Dataset.from_tensor_slices(x_train)\n",
        "train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)\n",
        "\n",
        "epochs = 3\n",
        "\n",
        "# Итерируем по эпохам.\n",
        "for epoch in range(epochs):\n",
        "  print('Start of epoch %d' % (epoch,))\n",
        "\n",
        "  # Итерируем по пакетам в датасете.\n",
        "  for step, x_batch_train in enumerate(train_dataset):\n",
        "    with tf.GradientTape() as tape:\n",
        "      reconstructed = vae(x_batch_train)\n",
        "      # Compute reconstruction loss\n",
        "      loss = mse_loss_fn(x_batch_train, reconstructed)\n",
        "      loss += sum(vae.losses)  # Add KLD regularization loss\n",
        "\n",
        "    grads = tape.gradient(loss, vae.trainable_weights)\n",
        "    optimizer.apply_gradients(zip(grads, vae.trainable_weights))\n",
        "\n",
        "    loss_metric(loss)\n",
        "\n",
        "    if step % 100 == 0:\n",
        "      print('step %s: mean loss = %s' % (step, loss_metric.result()))\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "5hgOl_y34NZD"
      },
      "source": [
        "Заметьте что поскольку VAE наследуется от `Model`, у него есть встроенные циклы обучения. Так что вы можете обучить его следующим образом:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Y153oEzk4Piz"
      },
      "outputs": [],
      "source": [
        "vae = VariationalAutoEncoder(784, 64, 32)\n",
        "\n",
        "optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)\n",
        "\n",
        "vae.compile(optimizer, loss=tf.keras.losses.MeanSquaredError())\n",
        "vae.fit(x_train, x_train, epochs=3, batch_size=64)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ZVkFme-U0IHb"
      },
      "source": [
        "### Помимо объектно-ориентированной разработки: Functional API\n",
        "\n",
        "Не слишком ли много в этом примере объектно-ориентированной разработки для вас? Вы также можете создавать модели, используя [Functional API](./functions.ipynb). Важно, что выбор одного стиля или другого не мешает вам сомещать компоненты, написанные в разных стилях: вы всегда можете сочетать и смешивать их.\n",
        "\n",
        "Например, вариант с Functional API переиспользует тот же слой `Sampling` который мы определили в вышеприведенном примере."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "1QzeXGAl3Uxn"
      },
      "outputs": [],
      "source": [
        "original_dim = 784\n",
        "intermediate_dim = 64\n",
        "latent_dim = 32\n",
        "\n",
        "# Определим модель энкодера.\n",
        "original_inputs = tf.keras.Input(shape=(original_dim,), name='encoder_input')\n",
        "x = layers.Dense(intermediate_dim, activation='relu')(original_inputs)\n",
        "z_mean = layers.Dense(latent_dim, name='z_mean')(x)\n",
        "z_log_var = layers.Dense(latent_dim, name='z_log_var')(x)\n",
        "z = Sampling()((z_mean, z_log_var))\n",
        "encoder = tf.keras.Model(inputs=original_inputs, outputs=z, name='encoder')\n",
        "\n",
        "# Определим модель декодера.\n",
        "latent_inputs = tf.keras.Input(shape=(latent_dim,), name='z_sampling')\n",
        "x = layers.Dense(intermediate_dim, activation='relu')(latent_inputs)\n",
        "outputs = layers.Dense(original_dim, activation='sigmoid')(x)\n",
        "decoder = tf.keras.Model(inputs=latent_inputs, outputs=outputs, name='decoder')\n",
        "\n",
        "# Определим модель VAE.\n",
        "outputs = decoder(z)\n",
        "vae = tf.keras.Model(inputs=original_inputs, outputs=outputs, name='vae')\n",
        "\n",
        "# Добавим KL-дивергенцию потери регуляризации.\n",
        "kl_loss = - 0.5 * tf.reduce_mean(\n",
        "    z_log_var - tf.square(z_mean) - tf.exp(z_log_var) + 1)\n",
        "vae.add_loss(kl_loss)\n",
        "\n",
        "# Обучим.\n",
        "optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)\n",
        "vae.compile(optimizer, loss=tf.keras.losses.MeanSquaredError())\n",
        "vae.fit(x_train, x_train, epochs=3, batch_size=64)"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "collapsed_sections": [],
      "name": "custom_layers_and_models.ipynb",
      "toc_visible": true
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
